/* -------------------------------------------------------------------------- *
   TestWinUnit - Maria Blees (maria.blees@microsoft.com)

   File: MainTest.cpp
   This file is for testing Main.cpp of WinUnit.exe.  The return value of the
   application is tested, and stderr and stdout are captured in expected failure
   cases.  An info line is displayed indicating what the expected output should be;
   then the relevant parts of the expected output are displayed, for visual
   inspection.
 * -------------------------------------------------------------------------- */

#include "WinUnit.h"
#include "TempFile.h"
#include "FileEnumerator.h"
#include "MockLogger.h"
#include "NulFile.h"
#include "ReadableTempFile.h"

#include <windows.h>

using namespace WinUnitLib;

namespace
{
    wchar_t s_testDirectoryBuffer[MAX_PATH] = L"";
    // This is here so there's something to set to NULL in test cleanup.
    wchar_t* s_testDirectory = NULL;
    DWORD RunProcess(wchar_t* appName, wchar_t* commandLine, 
        HANDLE stderrHandle = INVALID_HANDLE_VALUE, 
        HANDLE stdoutHandle = INVALID_HANDLE_VALUE);

    void DeleteAllFilesInDirectory(const wchar_t* directory);
}

FIXTURE(MainTest);

SETUP(MainTest)
{
    // Create temp directory
    WIN_ASSERT_WINAPI_SUCCESS(0 != ::GetTempPathW(MAX_PATH, s_testDirectoryBuffer));
    ::wcscat_s(s_testDirectoryBuffer, L"WinUnitTestDir\\");

    s_testDirectory = s_testDirectoryBuffer;
    WIN_ASSERT_WINAPI_SUCCESS(0 != ::CreateDirectoryW(s_testDirectory, NULL), 
        _T("Error creating directory %s. "), s_testDirectory);
}


TEARDOWN(MainTest)
{
    // Delete temp directory

    wchar_t* directory = s_testDirectory;
    s_testDirectory = NULL;

    DeleteAllFilesInDirectory(directory);

    // If there's a failure here, it could mean that you didn't close
    // some handle elsewhere and could indicate a bug.  Ideally all handles
    // would be forcibly closed to ensure proper cleanup, but this is overkill
    // considering if there's a failure something needs to be fixed anyway.
    WIN_ASSERT_WINAPI_SUCCESS(0 != ::RemoveDirectoryW(directory));
}

BEGIN_TEST(MainReturnsUsageErrorOnNoArgs)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following two output lines should be the start of the usage message.\n");
    for (int i = 0; i < 2; i++)
    {
        WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
    }
}
END_TEST

BEGIN_TEST(MainReturnsUsageErrorOnDashQuestionMark)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -?",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following two output lines should be the start of the usage message.\n");
    for (int i = 0; i < 2; i++)
    {
        WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
    }
}
END_TEST

BEGIN_TEST(MainReturnsUsageErrorOnSlashQuestionMark)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe /?",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following two output lines should be the start of the usage message.\n");
    for (int i = 0; i < 2; i++)
    {
        WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
    }
}
END_TEST


BEGIN_TEST(MainAcceptsAllArgs)
{
    DWORD retval = RunProcess(L"WinUnit.exe", 
        L"WinUnit.exe -p Blahblah -n -b -q --foo \"hello\" TestWinUnit.dll");
    WIN_ASSERT_EQUAL(0U, retval);
}
END_TEST

BEGIN_TEST(MainFailsWithNoFileOrDirectoryArg)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -p Blahblah -n -b -q",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that at least one file or directory is required.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsOnPrefixWithNoArg)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -p",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -p option requires an argument.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsOnVerbosityWithNoArg)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -v",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -v option requires an argument.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsOnVerbosityWithNegativeArg)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -v -1",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -v option requires a positive number.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainSucceedsOnNonnumericVerbosity)
{
    DWORD retval = RunProcess(L"WinUnit.exe", 
        L"WinUnit.exe -p Blahblah -v foo TestWinUnit.dll");
    WIN_ASSERT_EQUAL(0U, retval);
}
END_TEST

BEGIN_TEST(MainFailsOnOuputOptionWithNoArg)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -o",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -o option requires an argument.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsOnDoubleDashWithNoVariable)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe --",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -- option requires a variable name.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsOnDoubleDashWithNoArg)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe --foo",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that --var option requires an argument.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainProcessesDirectory)
{
    {
        // Just list test names
        DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -s .");
        WIN_ASSERT_EQUAL(0U, retval);
    }
    {
        // Only run tests starting with Blahblah (i.e. skip everyhing)
        DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -p Blahblah .");
        WIN_ASSERT_EQUAL(0U, retval);
    }
}
END_TEST

BEGIN_TEST(MainDoesNotAllowIgnoreTestPrefixWithoutListOnly)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -x .",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -x requires -s.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TESTF(MainCreatesOutputFileIfSpecified, MainTest)
{
    wchar_t outputFile[MAX_PATH] = L"";
    ::wcscpy_s(outputFile, ::s_testDirectory);
    ::wcscat_s(outputFile, L"MyOutput.txt");

    // (CreateProcess can only take MAX_PATH as the size of this argument.)
    wchar_t commandLine[MAX_PATH] = L"";
    ::swprintf_s(commandLine, L"WinUnit.exe -s -o %s .", outputFile);

    DWORD retval = RunProcess(L"WinUnit.exe", commandLine);
    WIN_ASSERT_EQUAL(0U, retval);

    WIN_ASSERT_WINAPI_SUCCESS(INVALID_FILE_ATTRIBUTES != 
        ::GetFileAttributesW(outputFile));
}
END_TESTF

BEGIN_TEST(MainFailsOnInvalidParameter)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -f .",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -f is an invalid parameter.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainDoesNotAllowPrefixOptionWithIgnoreTestPrefix)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -x -s -p Blahblah .",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -x and -p are incompatible.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainRaisesVerbosityForListOnly)
{
    // Can't really test for whether verbosity was raised but can at least
    // cover the line
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -s -v 0 .");
    WIN_ASSERT_EQUAL(0U, retval);
}
END_TEST

BEGIN_TEST(MainFailsOnInvalidOutputFile)
{
    ReadableTempFileT<200> stdoutCapture;
    ReadableTempFileT<200> stderrCapture;
    // This is expected to fail if you don't have admin rights to write to root!
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -o c:\\foo.txt -p Blahblah .",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval, _T("This was an attempt to write to the root of your C drive and was supposed to fail since you shouldn't have rights."));
    WIN_TRACE("INFO: The following line to stdout should indicate that an invalid output file was specified.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
    WIN_TRACE("INFO: The following line to stderr should indicate that the file could not be opened.\n");
    WIN_TRACE("OUTPUT: %s\n", stderrCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsOnInvalidCustomLogger)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -l foobar.dll -p Blahblah .",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that an invalid logger file was specified.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainDoesNotFailOnInvalidInputFile)
{
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe Blahblah.dll");
    WIN_ASSERT_EQUAL(0U, retval);
}
END_TEST

BEGIN_TEST(MainFailsOnLoggerArgWithNoParameter)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -l",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -l option requires an argument.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithLoggerArgUsedTwice)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -l foo -l bar",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -l option can only be used once.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithOutputArgUsedTwice)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -o foo -o bar",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -o option can only be used once.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithExactNamesAndPrefixOptionsUsedTogether)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -e ( foo ) -p foo bar.dll",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -e and -p options cannot be used together.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithExactNamesAndShowOptionsUsedTogether)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -e ( foo ) -s bar.dll",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate a parameter conflict.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithExactNamesAndIgnorePrefixOptionsUsedTogether)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -e ( foo ) -x foo bar.dll",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate a parameter conflict.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithExactNameWithNoOpenParen)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -e foo bar.dll",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -e option requires parens.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithExactNameWithNoClosedParen)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -e ( foo bar.dll",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -e option requires closed paren.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST

BEGIN_TEST(MainFailsWithExactNameWithNoSpaceAfterOpenParen)
{
    ReadableTempFileT<200> stdoutCapture;
    NulFile stderrCapture;
    DWORD retval = RunProcess(L"WinUnit.exe", L"WinUnit.exe -e (foo) bar.dll",
        stderrCapture, stdoutCapture);
    WIN_ASSERT_EQUAL((DWORD)-1, retval);
    WIN_TRACE("INFO: The following line should indicate that -e option requires space after open paren.\n");
    WIN_TRACE("OUTPUT: %s\n", stdoutCapture.ReadNonEmptyLine());
}
END_TEST


namespace
{
    // This function runs the given application with the given command line, 
    // redirecting stdout, stdin, and stderr to the specified handles or NUL.  
    // It returns whatever exit code was returned by the process.
    // Note: It waits "forever" for the process to return, so don't call this
    // on processes that aren't going to exit.
    DWORD RunProcess(wchar_t* appName, wchar_t* commandLine, 
        HANDLE stderrHandle /* INVALID_HANDLE_VALUE */, 
        HANDLE stdoutHandle /* INVALID_HANDLE_VALUE */)
    {
        PROCESS_INFORMATION processInfo = { 0 };
        STARTUPINFOW startupInfo = { 0 };

        // NulFile will be used for stdin, and stdout and/or stderr if they were not
        // specified in the arguments.
        NulFile nulFile;
        WIN_ASSERT_NOT_EQUAL(INVALID_HANDLE_VALUE, (HANDLE)nulFile);
        
        // I didn't make this customizable because I never use it--stdin is 
        // always NUL.
        startupInfo.hStdInput = nulFile;

        startupInfo.hStdError = 
            (stderrHandle != INVALID_HANDLE_VALUE) ?
            stderrHandle : nulFile;

        startupInfo.hStdOutput = 
            (stdoutHandle != INVALID_HANDLE_VALUE) ?
            stdoutHandle : nulFile;

        startupInfo.dwFlags = STARTF_USESTDHANDLES;

        WIN_ASSERT_WINAPI_SUCCESS(0 != ::CreateProcessW(
            appName, 
            commandLine,        // first token should be appName, repeated
            NULL,               // process gets default security
            NULL,               // thread gets default security
            TRUE,               // should inherit handles from parent
            0,                  // default creation flags
            NULL,               // use environment of calling process
            NULL,               // starting directory is same as calling process
            &startupInfo,
            &processInfo));

        WIN_ASSERT_EQUAL(WAIT_OBJECT_0, ::WaitForSingleObject(processInfo.hProcess, INFINITE));

        DWORD exitCode = 0;
        WIN_ASSERT_WINAPI_SUCCESS(0 != ::GetExitCodeProcess(processInfo.hProcess, &exitCode));

        WIN_ASSERT_WINAPI_SUCCESS(0 != ::CloseHandle(processInfo.hProcess));
        WIN_ASSERT_WINAPI_SUCCESS(0 != ::CloseHandle(processInfo.hThread));

        return exitCode;
    }

    void DeleteAllFilesInDirectory(const wchar_t* directory)
    {
        MockLogger logger;
        FileEnumerator fileEnumerator(logger, directory, L"*");
        for(const wchar_t* fileName = fileEnumerator.GetFirst();
            fileName != NULL;
            fileName = fileEnumerator.GetNext())
        {
            wchar_t fullPathName[MAX_PATH] = L"";

            // FileEnumerator just returns the file names without their 
            // qualifying path.
            ::wcscpy_s(fullPathName, ARRAYSIZE(fullPathName), directory);
            if (directory[::wcslen(directory) - 1] != L'\\')
            {
                ::wcscat_s(fullPathName, ARRAYSIZE(fullPathName), L"\\");
            }
            ::wcscat_s(fullPathName, fileName);

            WIN_ASSERT_TRUE(::DeleteFileW(fullPathName));
        }
    }
}

